import { EventDispatcher, MOUSE, Quaternion, Spherical, TOUCH, Vector2, Vector3, Plane, Ray, MathUtils } from 'three'

// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
//    Orbit - left mouse / touch: one-finger move
//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
//    Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move

const _changeEvent = { type: 'change' }
const _startEvent = { type: 'start' }
const _endEvent = { type: 'end' }
const _ray = new Ray()
const _plane = new Plane()
const TILT_LIMIT = Math.cos(70 * MathUtils.DEG2RAD)

class OrbitControls extends EventDispatcher {
	isOrbitControls = true
	constructor(object, domElement) {
		super()
		this.eventType = null
		this.object = object
		this.domElement = domElement
		this.domElement.style.touchAction = 'none' // disable touch scroll

		// Set to false to disable this control
		this.enabled = true

		// "target" sets the location of focus, where the object orbits around
		this.target = new Vector3()

		//"eventTarget" sets the location of focus, where the object orbits around
		this.eventTarget = new Vector3()

		// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect
		this.cursor = new Vector3()

		// How far you can dolly in and out ( PerspectiveCamera only )
		this.minDistance = 0
		this.maxDistance = Infinity

		// How far you can zoom in and out ( OrthographicCamera only )
		this.minZoom = 0
		this.maxZoom = Infinity

		// Limit camera target within a spherical area around the cursor
		this.minTargetRadius = 0
		this.maxTargetRadius = Infinity

		// How far you can orbit vertically, upper and lower limits.
		// Range is 0 to Math.PI radians.
		this.minPolarAngle = 0 // radians
		this.maxPolarAngle = Math.PI // radians

		// How far you can orbit horizontally, upper and lower limits.
		// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
		this.minAzimuthAngle = -Infinity // radians
		this.maxAzimuthAngle = Infinity // radians

		// Set to true to enable damping (inertia)
		// If damping is enabled, you must call controls.update() in your animation loop
		this.enableDamping = false
		this.dampingFactor = 0.05

		// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
		// Set to false to disable zooming
		this.enableZoom = true
		this.zoomSpeed = 1.0

		// Set to false to disable rotating
		this.enableRotate = true
		this.rotateSpeed = 1.0

		// Set to false to disable panning
		this.enablePan = true
		this.panSpeed = 1.0
		this.screenSpacePanning = true // if false, pan orthogonal to world-space direction camera.up
		this.keyPanSpeed = 7.0 // pixels moved per arrow key push
		this.zoomToCursor = false

		// Set to true to automatically rotate around the target
		// If auto-rotate is enabled, you must call controls.update() in your animation loop
		this.autoRotate = false
		this.autoRotateSpeed = 2.0 // 30 seconds per orbit when fps is 60

		// The four arrow keys
		this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }

		// Mouse buttons
		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }

		// Touch fingers
		this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }

		// for reset
		this.target0 = this.target.clone()
		this.position0 = this.object.position.clone()
		this.zoom0 = this.object.zoom

		// the target DOM element for key events
		this._domElementKeyEvents = null

		//
		// public methods
		//

		this.getPolarAngle = function () {
			return spherical.phi
		}

		this.getAzimuthalAngle = function () {
			return spherical.theta
		}

		this.getDistance = function () {
			return this.object.position.distanceTo(this.target)
		}

		this.listenToKeyEvents = function (domElement) {
			domElement.addEventListener('keydown', onKeyDown)
			this._domElementKeyEvents = domElement
		}

		this.stopListenToKeyEvents = function () {
			this._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
			this._domElementKeyEvents = null
		}

		this.saveState = function () {
			scope.target0.copy(scope.target)
			scope.position0.copy(scope.object.position)
			scope.zoom0 = scope.object.zoom
		}

		this.reset = function () {
			scope.target.copy(scope.target0)
			scope.object.position.copy(scope.position0)
			scope.object.zoom = scope.zoom0

			scope.object.updateProjectionMatrix()
			scope.dispatchEvent(_changeEvent)

			scope.update()

			state = STATE.NONE
		}

		// this method is exposed, but perhaps it would be better if we can make it private...
		this.update = (function () {
			const offset = new Vector3()

			// 所以camera.up是轨道轴  Quaternion四元数
			const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0))
			// 翻转该四元数
			const quatInverse = quat.clone().invert()

			const lastPosition = new Vector3()
			const lastQuaternion = new Quaternion()
			const lastTargetPosition = new Vector3()

			const twoPI = 2 * Math.PI

			return function update(deltaTime = null) {
				// 获取相机位置
				const position = scope.object.position

				// 用当前相机位置减去焦点位置，获得向量
				offset.copy(position).sub(scope.target)

				// rotate offset to "y-axis-is-up" space  旋转偏移到y-axis-is-up
				offset.applyQuaternion(quat)

				// angle from z-axis around y-axis，通过获得的向量建立球坐标系
				spherical.setFromVector3(offset)

				// 自动旋转
				if (scope.autoRotate && state === STATE.NONE) {
					rotateLeft(getAutoRotationAngle(deltaTime))
				}

				if (scope.enableDamping) {
					// theta 绕 y (up) 轴的赤道角(方位角)（以弧度为单位）
					spherical.theta += sphericalDelta.theta * scope.dampingFactor
					// phi - 与 y (up) 轴的极角（以弧度为单位）。 默认值为 0。
					spherical.phi += sphericalDelta.phi * scope.dampingFactor
				} else {
					spherical.theta += sphericalDelta.theta
					spherical.phi += sphericalDelta.phi
				}

				// restrict theta to be between desired limits
				let min = scope.minAzimuthAngle
				let max = scope.maxAzimuthAngle

				if (isFinite(min) && isFinite(max)) {
					if (min < -Math.PI) min += twoPI
					else if (min > Math.PI) min -= twoPI

					if (max < -Math.PI) max += twoPI
					else if (max > Math.PI) max -= twoPI

					if (min <= max) {
						spherical.theta = Math.max(min, Math.min(max, spherical.theta))
					} else {
						spherical.theta = spherical.theta > (min + max) / 2 ? Math.max(min, spherical.theta) : Math.min(max, spherical.theta)
					}
				}

				// 控制球坐标系极坐标上下限，避免变换到phi的极点
				spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi))
				spherical.makeSafe()

				// move target to panned location 移动时为移动target
				if (scope.enableDamping === true) {
					scope.target.addScaledVector(panOffset, scope.dampingFactor)
				} else {
					scope.target.add(panOffset)
				}

				// Limit the target distance from the cursor to create a sphere around the center of interest
				scope.target.sub(scope.cursor)
				scope.target.clampLength(scope.minTargetRadius, scope.maxTargetRadius)
				scope.target.add(scope.cursor)

				let zoomChanged = false
				// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
				// we adjust zoom later in these cases
				if ((scope.zoomToCursor && performCursorZoom) || scope.object.isOrthographicCamera) {
					spherical.radius = clampDistance(spherical.radius)
				} else {
					const prevRadius = spherical.radius
					spherical.radius = clampDistance(spherical.radius * scale)
					zoomChanged = prevRadius != spherical.radius
				}

				// 再将球坐标转换为向量
				offset.setFromSpherical(spherical)

				// rotate offset back to "camera-up-vector-is-up" space
				offset.applyQuaternion(quatInverse)

				// 相机的位置也就是焦点位置加上球坐标的偏移量
				position.copy(scope.target).add(offset)

				scope.object.lookAt(scope.target)

				if (scope.enableDamping === true) {
					sphericalDelta.theta *= 1 - scope.dampingFactor
					sphericalDelta.phi *= 1 - scope.dampingFactor

					panOffset.multiplyScalar(1 - scope.dampingFactor)
				} else {
					sphericalDelta.set(0, 0, 0)

					panOffset.set(0, 0, 0)
				}

				// adjust camera position
				if (scope.zoomToCursor && performCursorZoom) {
					let newRadius = null
					// 透视相机
					if (scope.object.isPerspectiveCamera) {
						// move the camera down the pointer ray
						// this method avoids floating point error
						const prevRadius = offset.length()
						newRadius = clampDistance(prevRadius * scale)

						const radiusDelta = prevRadius - newRadius
						scope.object.position.addScaledVector(dollyDirection, radiusDelta)
						scope.object.updateMatrixWorld()

						zoomChanged = !!radiusDelta
						//正交相机
					} else if (scope.object.isOrthographicCamera) {
						// adjust the ortho camera position based on zoom changes
						const mouseBefore = new Vector3(mouse.x, mouse.y, 0)
						mouseBefore.unproject(scope.object)

						const prevZoom = scope.object.zoom
						scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale))

						scope.object.updateProjectionMatrix()

						zoomChanged = prevZoom !== scope.object.zoom

						const mouseAfter = new Vector3(mouse.x, mouse.y, 0)
						mouseAfter.unproject(scope.object)

						scope.object.position.sub(mouseAfter).add(mouseBefore)
						scope.object.updateMatrixWorld()

						newRadius = offset.length()
					} else {
						console.warn('WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.')
						scope.zoomToCursor = false
					}

					// handle the placement of the target
					if (newRadius !== null) {
						if (this.screenSpacePanning) {
							// position the orbit target in front of the new camera position
							scope.target.set(0, 0, -1).transformDirection(scope.object.matrix).multiplyScalar(newRadius).add(scope.object.position)
						} else {
							// get the ray and translation plane to compute target
							_ray.origin.copy(scope.object.position)
							_ray.direction.set(0, 0, -1).transformDirection(scope.object.matrix)

							// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
							// extremely large values
							if (Math.abs(scope.object.up.dot(_ray.direction)) < TILT_LIMIT) {
								object.lookAt(scope.target)
							} else {
								_plane.setFromNormalAndCoplanarPoint(scope.object.up, scope.target)
								_ray.intersectPlane(_plane, scope.target)
							}
						}
					}
				} else if (scope.object.isOrthographicCamera) {
					const prevZoom = scope.object.zoom
					scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale))

					if (prevZoom !== scope.object.zoom) {
						// 更新摄像机投影矩阵
						scope.object.updateProjectionMatrix()
						zoomChanged = true
					}
				}

				scale = 1
				performCursorZoom = false

				// update condition is:
				// min(camera displacement, camera rotation in radians)^2 > EPS
				// using small-angle approximation cos(x/2) = 1 - x^2 / 8

				if (
					zoomChanged ||
					lastPosition.distanceToSquared(scope.object.position) > EPS ||
					8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS ||
					lastTargetPosition.distanceToSquared(scope.target) > EPS
				) {
					scope.dispatchEvent(_changeEvent)

					lastPosition.copy(scope.object.position)
					lastQuaternion.copy(scope.object.quaternion)
					lastTargetPosition.copy(scope.target)

					return true
				}

				return false
			}
		})()

		this.dispose = function () {
			scope.domElement.removeEventListener('contextmenu', onContextMenu)

			scope.domElement.removeEventListener('pointerdown', onPointerDown)
			scope.domElement.removeEventListener('pointercancel', onPointerUp)
			scope.domElement.removeEventListener('wheel', onMouseWheel)

			scope.domElement.removeEventListener('pointermove', onPointerMove)
			scope.domElement.removeEventListener('pointerup', onPointerUp)

			const document = scope.domElement.getRootNode() // offscreen canvas compatibility

			document.removeEventListener('keydown', interceptControlDown, { capture: true })

			if (scope._domElementKeyEvents !== null) {
				scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
				scope._domElementKeyEvents = null
			}

			//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
		}

		//
		// internals
		//

		const scope = this

		const STATE = {
			NONE: -1,
			ROTATE: 0,
			DOLLY: 1,
			PAN: 2,
			TOUCH_ROTATE: 3,
			TOUCH_PAN: 4,
			TOUCH_DOLLY_PAN: 5,
			TOUCH_DOLLY_ROTATE: 6
		}

		let state = STATE.NONE

		const EPS = 0.000001

		// current position in spherical coordinates
		const spherical = new Spherical()
		const sphericalDelta = new Spherical()

		let scale = 1
		const panOffset = new Vector3()

		const rotateStart = new Vector2()
		const rotateEnd = new Vector2()
		const rotateDelta = new Vector2()

		const panStart = new Vector2()
		const panEnd = new Vector2()
		const panDelta = new Vector2()

		const dollyStart = new Vector2()
		const dollyEnd = new Vector2()
		const dollyDelta = new Vector2()

		const dollyDirection = new Vector3()
		const mouse = new Vector2()
		let performCursorZoom = false

		const pointers = []
		const pointerPositions = {}

		let controlActive = false

		function getAutoRotationAngle(deltaTime) {
			if (deltaTime !== null) {
				return ((2 * Math.PI) / 60) * scope.autoRotateSpeed * deltaTime
			} else {
				return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed
			}
		}

		function getZoomScale(delta) {
			const normalizedDelta = Math.abs(delta * 0.01)
			return Math.pow(0.95, scope.zoomSpeed * normalizedDelta)
		}

		function rotateLeft(angle) {
			sphericalDelta.theta -= angle
		}

		function rotateUp(angle) {
			sphericalDelta.phi -= angle
		}

		const panLeft = (function () {
			const v = new Vector3()

			return function panLeft(distance, objectMatrix) {
				v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix
				v.multiplyScalar(-distance)

				panOffset.add(v)
			}
		})()

		const panUp = (function () {
			const v = new Vector3()

			return function panUp(distance, objectMatrix) {
				// screenSpacePanning定义camera平移时的移动方式
				// true: 以屏幕空间为主，false: 在与camera的up轴相交的平面中移动
				if (scope.screenSpacePanning === true) {
					// 矩阵列的xyz对应索引为0，1，2
					// y轴分量
					v.setFromMatrixColumn(objectMatrix, 1)
				} else {
					// 获取与相机x轴以及up轴垂直的平面
					v.setFromMatrixColumn(objectMatrix, 0)
					v.crossVectors(scope.object.up, v)
				}

				// 变化幅度
				v.multiplyScalar(distance)

				panOffset.add(v)
			}
		})()

		// deltaX and deltaY are in pixels; right and down are positive
		const pan = (function () {
			const offset = new Vector3()

			return function pan(deltaX, deltaY) {
				const element = scope.domElement

				if (scope.object.isPerspectiveCamera) {
					// perspective
					const position = scope.object.position
					offset.copy(position).sub(scope.target)
					let targetDistance = offset.length()

					// half of the fov is center to top of screen
					targetDistance *= Math.tan(((scope.object.fov / 2) * Math.PI) / 180.0)

					// we use only clientHeight here so aspect ratio does not distort speed
					panLeft((2 * deltaX * targetDistance) / element.clientHeight, scope.object.matrix)
					panUp((2 * deltaY * targetDistance) / element.clientHeight, scope.object.matrix)
				} else if (scope.object.isOrthographicCamera) {
					// orthographic
					panLeft((deltaX * (scope.object.right - scope.object.left)) / scope.object.zoom / element.clientWidth, scope.object.matrix)
					panUp((deltaY * (scope.object.top - scope.object.bottom)) / scope.object.zoom / element.clientHeight, scope.object.matrix)
				} else {
					// camera neither orthographic nor perspective
					console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.')
					scope.enablePan = false
				}
			}
		})()

		function dollyOut(dollyScale) {
			scope.eventType = 'zoomOut'
			if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) {
				scale /= dollyScale
			} else {
				console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
				scope.enableZoom = false
			}
		}

		function dollyIn(dollyScale) {
			scope.eventType = 'zoomIn'
			if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) {
				scale *= dollyScale
			} else {
				console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
				scope.enableZoom = false
			}
		}

		function updateZoomParameters(x, y) {
			if (!scope.zoomToCursor) {
				return
			}

			performCursorZoom = true

			const rect = scope.domElement.getBoundingClientRect()
			const dx = x - rect.left
			const dy = y - rect.top
			const w = rect.width
			const h = rect.height

			mouse.x = (dx / w) * 2 - 1
			mouse.y = -(dy / h) * 2 + 1

			dollyDirection.set(mouse.x, mouse.y, 1).unproject(scope.object).sub(scope.object.position).normalize()
		}

		function clampDistance(dist) {
			return Math.max(scope.minDistance, Math.min(scope.maxDistance, dist))
		}

		//
		// event callbacks - update the object state
		//

		function handleMouseDownRotate(event) {
			rotateStart.set(event.clientX, event.clientY)
		}

		function handleMouseDownDolly(event) {
			updateZoomParameters(event.clientX, event.clientX)
			dollyStart.set(event.clientX, event.clientY)
		}

		function handleMouseDownPan(event) {
			panStart.set(event.clientX, event.clientY)
		}

		function handleMouseMoveRotate(event) {
			// 获取鼠标移动时的位置
			rotateEnd.set(event.clientX, event.clientY)
			// 获取鼠标相对于上一个鼠标位置的向量
			rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)

			const element = scope.domElement

			// 定义每一次旋转的角度
			// 左右旋转为甚不除以宽度达到归一化，因为透视相机的高度是固定的，所以使用高度
			// 事实上无论除什么都是可以的，只是为了归一化方便控制旋转范围而已
			rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height

			rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight)

			// 控制旋转匀速，避免因鼠标移动距离是的旋转速度越来越快
			rotateStart.copy(rotateEnd)

			scope.update()
		}

		function handleMouseMoveDolly(event) {
			dollyEnd.set(event.clientX, event.clientY)

			dollyDelta.subVectors(dollyEnd, dollyStart)

			if (dollyDelta.y > 0) {
				dollyOut(getZoomScale(dollyDelta.y))
			} else if (dollyDelta.y < 0) {
				dollyIn(getZoomScale(dollyDelta.y))
			}

			dollyStart.copy(dollyEnd)

			scope.update()
		}

		function handleMouseMovePan(event) {
			panEnd.set(event.clientX, event.clientY)

			panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)

			pan(panDelta.x, panDelta.y)

			panStart.copy(panEnd)

			scope.update()
		}

		function handleMouseWheel(event) {
			updateZoomParameters(event.clientX, event.clientY)
			if (event.deltaY < 0) {
				dollyIn(getZoomScale(event.deltaY))
			} else if (event.deltaY > 0) {
				dollyOut(getZoomScale(event.deltaY))
			}

			scope.update()
		}

		function handleKeyDown(event) {
			let needsUpdate = false

			switch (event.code) {
				case scope.keys.UP:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						rotateUp((2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight)
					} else {
						pan(0, scope.keyPanSpeed)
					}

					needsUpdate = true
					break

				case scope.keys.BOTTOM:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						rotateUp((-2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight)
					} else {
						pan(0, -scope.keyPanSpeed)
					}

					needsUpdate = true
					break

				case scope.keys.LEFT:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						rotateLeft((2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight)
					} else {
						pan(scope.keyPanSpeed, 0)
					}

					needsUpdate = true
					break

				case scope.keys.RIGHT:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						rotateLeft((-2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight)
					} else {
						pan(-scope.keyPanSpeed, 0)
					}

					needsUpdate = true
					break
			}

			if (needsUpdate) {
				// prevent the browser from scrolling on cursor keys
				event.preventDefault()

				scope.update()
			}
		}

		function handleTouchStartRotate(event) {
			if (pointers.length === 1) {
				rotateStart.set(event.pageX, event.pageY)
			} else {
				const position = getSecondPointerPosition(event)

				const x = 0.5 * (event.pageX + position.x)
				const y = 0.5 * (event.pageY + position.y)

				rotateStart.set(x, y)
			}
		}

		function handleTouchStartPan(event) {
			if (pointers.length === 1) {
				panStart.set(event.pageX, event.pageY)
			} else {
				const position = getSecondPointerPosition(event)

				const x = 0.5 * (event.pageX + position.x)
				const y = 0.5 * (event.pageY + position.y)

				panStart.set(x, y)
			}
		}

		function handleTouchStartDolly(event) {
			const position = getSecondPointerPosition(event)

			const dx = event.pageX - position.x
			const dy = event.pageY - position.y

			const distance = Math.sqrt(dx * dx + dy * dy)

			dollyStart.set(0, distance)
		}

		function handleTouchStartDollyPan(event) {
			if (scope.enableZoom) handleTouchStartDolly(event)

			if (scope.enablePan) handleTouchStartPan(event)
		}

		function handleTouchStartDollyRotate(event) {
			if (scope.enableZoom) handleTouchStartDolly(event)

			if (scope.enableRotate) handleTouchStartRotate(event)
		}

		function handleTouchMoveRotate(event) {
			if (pointers.length == 1) {
				rotateEnd.set(event.pageX, event.pageY)
			} else {
				const position = getSecondPointerPosition(event)

				const x = 0.5 * (event.pageX + position.x)
				const y = 0.5 * (event.pageY + position.y)

				rotateEnd.set(x, y)
			}

			rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)

			const element = scope.domElement

			rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height

			rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight)

			rotateStart.copy(rotateEnd)
		}

		function handleTouchMovePan(event) {
			if (pointers.length === 1) {
				panEnd.set(event.pageX, event.pageY)
			} else {
				const position = getSecondPointerPosition(event)

				const x = 0.5 * (event.pageX + position.x)
				const y = 0.5 * (event.pageY + position.y)

				panEnd.set(x, y)
			}

			panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)

			pan(panDelta.x, panDelta.y)

			panStart.copy(panEnd)
		}

		function handleTouchMoveDolly(event) {
			const position = getSecondPointerPosition(event)

			const dx = event.pageX - position.x
			const dy = event.pageY - position.y

			const distance = Math.sqrt(dx * dx + dy * dy)

			dollyEnd.set(0, distance)

			dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed))

			dollyOut(dollyDelta.y)

			dollyStart.copy(dollyEnd)

			const centerX = (event.pageX + position.x) * 0.5
			const centerY = (event.pageY + position.y) * 0.5

			updateZoomParameters(centerX, centerY)
		}

		function handleTouchMoveDollyPan(event) {
			if (scope.enableZoom) handleTouchMoveDolly(event)

			if (scope.enablePan) handleTouchMovePan(event)
		}

		function handleTouchMoveDollyRotate(event) {
			if (scope.enableZoom) handleTouchMoveDolly(event)

			if (scope.enableRotate) handleTouchMoveRotate(event)
		}

		//
		// event handlers - FSM: listen for events and reset state
		//

		function onPointerDown(event) {
			if (scope.enabled === false) return

			if (pointers.length === 0) {
				scope.domElement.setPointerCapture(event.pointerId)

				scope.domElement.addEventListener('pointermove', onPointerMove)
				scope.domElement.addEventListener('pointerup', onPointerUp)
			}

			//

			if (isTrackingPointer(event)) return

			//

			addPointer(event)

			if (event.pointerType === 'touch') {
				onTouchStart(event)
			} else {
				onMouseDown(event)
			}
		}

		function onPointerMove(event) {
			if (scope.enabled === false) return

			if (event.pointerType === 'touch') {
				onTouchMove(event)
			} else {
				onMouseMove(event)
			}
		}

		function onPointerUp(event) {
			removePointer(event)

			switch (pointers.length) {
				case 0:
					scope.domElement.releasePointerCapture(event.pointerId)

					scope.domElement.removeEventListener('pointermove', onPointerMove)
					scope.domElement.removeEventListener('pointerup', onPointerUp)

					scope.dispatchEvent(_endEvent)

					state = STATE.NONE

					break

				case 1:
					const pointerId = pointers[0]
					const position = pointerPositions[pointerId]

					// minimal placeholder event - allows state correction on pointer-up
					onTouchStart({ pointerId: pointerId, pageX: position.x, pageY: position.y })

					break
			}
		}

		function onMouseDown(event) {
			let mouseAction

			switch (event.button) {
				case 0:
					mouseAction = scope.mouseButtons.LEFT
					break

				case 1:
					mouseAction = scope.mouseButtons.MIDDLE
					break

				case 2:
					mouseAction = scope.mouseButtons.RIGHT
					break

				default:
					mouseAction = -1
			}

			switch (mouseAction) {
				case MOUSE.DOLLY:
					if (scope.enableZoom === false) return

					handleMouseDownDolly(event)

					state = STATE.DOLLY

					break

				case MOUSE.ROTATE:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						if (scope.enablePan === false) return

						scope.eventType = 'pan'

						handleMouseDownPan(event)

						state = STATE.PAN
					} else {
						if (scope.enableRotate === false) return

						scope.eventType = 'rotate'

						handleMouseDownRotate(event)

						state = STATE.ROTATE
					}

					break

				case MOUSE.PAN:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						if (scope.enableRotate === false) return

						scope.eventType = 'rotate'

						handleMouseDownRotate(event)

						state = STATE.ROTATE
					} else {
						if (scope.enablePan === false) return

						scope.eventType = 'pan'

						handleMouseDownPan(event)

						state = STATE.PAN
					}

					break

				default:
					state = STATE.NONE
			}

			if (state !== STATE.NONE) {
				scope.dispatchEvent(_startEvent)
			}
		}

		function onMouseMove(event) {
			switch (state) {
				case STATE.ROTATE:
					if (scope.enableRotate === false) return

					handleMouseMoveRotate(event)

					break

				case STATE.DOLLY:
					if (scope.enableZoom === false) return

					handleMouseMoveDolly(event)

					break

				case STATE.PAN:
					if (scope.enablePan === false) return

					handleMouseMovePan(event)

					break
			}
		}

		function onMouseWheel(event) {
			if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE) return

			event.preventDefault()

			scope.dispatchEvent(_startEvent)

			handleMouseWheel(customWheelEvent(event))

			scope.dispatchEvent(_endEvent)
		}

		function customWheelEvent(event) {
			const mode = event.deltaMode

			// minimal wheel event altered to meet delta-zoom demand
			const newEvent = {
				clientX: event.clientX,
				clientY: event.clientY,
				deltaY: event.deltaY
			}

			switch (mode) {
				case 1: // LINE_MODE
					newEvent.deltaY *= 16
					break

				case 2: // PAGE_MODE
					newEvent.deltaY *= 100
					break
			}

			// detect if event was triggered by pinching
			if (event.ctrlKey && !controlActive) {
				newEvent.deltaY *= 10
			}

			return newEvent
		}

		function interceptControlDown(event) {
			if (event.key === 'Control') {
				controlActive = true

				const document = scope.domElement.getRootNode() // offscreen canvas compatibility

				document.addEventListener('keyup', interceptControlUp, { passive: true, capture: true })
			}
		}

		function interceptControlUp(event) {
			if (event.key === 'Control') {
				controlActive = false

				const document = scope.domElement.getRootNode() // offscreen canvas compatibility

				document.removeEventListener('keyup', interceptControlUp, { passive: true, capture: true })
			}
		}

		function onKeyDown(event) {
			if (scope.enabled === false || scope.enablePan === false) return

			handleKeyDown(event)
		}

		function onTouchStart(event) {
			trackPointer(event)

			switch (pointers.length) {
				case 1:
					switch (scope.touches.ONE) {
						case TOUCH.ROTATE:
							if (scope.enableRotate === false) return
							scope.eventType = 'rotate'
							handleTouchStartRotate(event)

							state = STATE.TOUCH_ROTATE

							break

						case TOUCH.PAN:
							if (scope.enablePan === false) return
							scope.eventType = 'pan'
							handleTouchStartPan(event)

							state = STATE.TOUCH_PAN

							break

						default:
							state = STATE.NONE
					}

					break

				case 2:
					switch (scope.touches.TWO) {
						case TOUCH.DOLLY_PAN:
							if (scope.enableZoom === false && scope.enablePan === false) return
							handleTouchStartDollyPan(event)
							scope.eventType = 'pan'
							state = STATE.TOUCH_DOLLY_PAN

							break

						case TOUCH.DOLLY_ROTATE:
							if (scope.enableZoom === false && scope.enableRotate === false) return
							scope.eventType = 'rotate'
							handleTouchStartDollyRotate(event)

							state = STATE.TOUCH_DOLLY_ROTATE

							break

						default:
							state = STATE.NONE
					}

					break

				default:
					state = STATE.NONE
			}

			if (state !== STATE.NONE) {
				scope.dispatchEvent(_startEvent)
			}
		}

		function onTouchMove(event) {
			trackPointer(event)

			switch (state) {
				case STATE.TOUCH_ROTATE:
					if (scope.enableRotate === false) return
					handleTouchMoveRotate(event)

					scope.update()

					break

				case STATE.TOUCH_PAN:
					if (scope.enablePan === false) return

					handleTouchMovePan(event)

					scope.update()

					break

				case STATE.TOUCH_DOLLY_PAN:
					if (scope.enableZoom === false && scope.enablePan === false) return

					handleTouchMoveDollyPan(event)

					scope.update()

					break

				case STATE.TOUCH_DOLLY_ROTATE:
					if (scope.enableZoom === false && scope.enableRotate === false) return

					handleTouchMoveDollyRotate(event)

					scope.update()

					break

				default:
					state = STATE.NONE
			}
		}

		function onContextMenu(event) {
			if (scope.enabled === false) return

			event.preventDefault()
		}

		function addPointer(event) {
			pointers.push(event.pointerId)
		}

		function removePointer(event) {
			delete pointerPositions[event.pointerId]

			for (let i = 0; i < pointers.length; i++) {
				if (pointers[i] == event.pointerId) {
					pointers.splice(i, 1)
					return
				}
			}
		}

		function isTrackingPointer(event) {
			for (let i = 0; i < pointers.length; i++) {
				if (pointers[i] == event.pointerId) return true
			}

			return false
		}

		function trackPointer(event) {
			let position = pointerPositions[event.pointerId]

			if (position === undefined) {
				position = new Vector2()
				pointerPositions[event.pointerId] = position
			}

			position.set(event.pageX, event.pageY)
		}

		function getSecondPointerPosition(event) {
			const pointerId = event.pointerId === pointers[0] ? pointers[1] : pointers[0]

			return pointerPositions[pointerId]
		}

		//

		scope.domElement.addEventListener('contextmenu', onContextMenu)
		scope.domElement.addEventListener('pointerdown', onPointerDown)
		scope.domElement.addEventListener('pointercancel', onPointerUp)
		scope.domElement.addEventListener('wheel', onMouseWheel, { passive: false })

		const document = scope.domElement.getRootNode() // offscreen canvas compatibility

		document.addEventListener('keydown', interceptControlDown, { passive: true, capture: true })

		// force an update at start
		this.update()
	}
}

export { OrbitControls }
